﻿namespace CommonLibrary.WorkFlow
{
    using CommonHelper;
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.IO;
    using System.Linq;
    using System.Web;
    using System.Xml;
    using System.Xml.Linq;

    public enum StepType
    {
        None = 0,
        Task = 1,
        Condition = 2,
        Switch = 3
    }

    public class Step
    {
        public int StepId { get; set; }
        public int TaskId { get; set; }
        public string Name { get; set; }
    }

    public class Flowchart
    {
        private class ConditionNext
        {
            public string Condition { get; set; }
            public int? TrueNext { get; set; }
            public int? FalseNext { get; set; }
        }

        private class SwitchNext
        {
            public string InputExpression { get; set; }
            public int? DefaultNext { get; set; }
            public Dictionary<string, int?> KeysNext { get; set; }
        }

        private class _Step
        {
            private int _id;
            public int Id
            {
                get { return _id; }
                set { _id = value; }
            }

            private StepType _type;

            public StepType Type
            {
                get { return _type; }
                set { _type = value; }
            }
            private string _name;

            public string Name
            {
                get { return _name; }
                set { _name = value; }
            }
            private int? _taskId;

            public int? TaskId
            {
                get { return _taskId; }
                set { _taskId = value; }
            }
            private object _next;

            public object Next
            {
                get { return _next; }
                set { _next = value; }
            }

            public _Step()
            {
            }

            public _Step(int id, StepType type = StepType.None)
            {
                _id = id;
                _type = type;
            }
        }

        public delegate bool CheckCallback(string condition);
        public delegate string GetInput(string expression);

        private XDocument config;
        private Dictionary<int, _Step> steps;
        private int start;

        public CheckCallback OnCondition { get; set; }
        public GetInput OnGetInput { get; set; }

        public Flowchart(
            string filename,
            CheckCallback onCondition,
            GetInput onGetInput)
        {
            this.config = XDocument.Parse(File.ReadAllText(filename));
            this.steps = new Dictionary<int, _Step>();
            this.OnCondition = onCondition;
            this.OnGetInput = onGetInput;

            this.Load();
        }

        public Step GetNext(int? currentStepId)
        {
            if (currentStepId == null)
            {
                return GetStep(start);
            }

            var current = steps[currentStepId.Value];
            if (current.Type == StepType.Task)
            {
                if (current.Next == null)
                    return null;
                return GetStep((int)current.Next);
            }

            throw new ArgumentException("step");
        }

        #region Initialize
        private void Load()
        {
            if (this.config == null)
                throw new ArgumentException("config");

            this.start = config.Root.Element("start").Value.ToInt32();
            var nodes = config.Root.Elements("step").ToList();

            foreach (var node in nodes)
            {
                var id = node.Attribute("id").Value.ToInt32();
                var type = (StepType)node.Attribute("type").Value.ToInt32();
                var step = new _Step(id, type);

                switch (type)
                {
                    case StepType.Task:
                        step.Name = node.Element("name").Value;
                        step.TaskId = node.Element("task").Value.ToInt32();
                        step.Next = GetNodeValue(node.Element("next"));
                        break;

                    case StepType.Condition:
                        step.Next = GetConditionNext(node);
                        break;

                    case StepType.Switch:
                        step.Next = GetSwitchNext(node);
                        break;
                }

                steps[id] = step;
            }
        }

        private ConditionNext GetConditionNext(XElement node)
        {
            var result = new ConditionNext()
            {
                Condition = node.Element("next").Element("condition").Value.Trim(),
                TrueNext = GetNodeValue(node.Element("next").Element("true")),
                FalseNext = GetNodeValue(node.Element("next").Element("false"))
            };

            return result;
        }

        private SwitchNext GetSwitchNext(XElement node)
        {
            var result = new SwitchNext()
            {
                InputExpression = node.Element("next").Element("input").Value.Trim(),
                DefaultNext = GetNodeValue(node.Element("next").Element("default"))
            };

            result.KeysNext = new Dictionary<string, int?>();

            var query = from p in node.Element("next").Elements("case")
                        select new
                        {
                            Key = p.Attribute("key").Value,
                            Value = GetNodeValue(p)
                        };

            foreach (var p in query)
            {
                result.KeysNext.Add(p.Key, p.Value);
            }

            return result;
        }
        #endregion

        private Step GetStep(int stepId)
        {
            var step = steps[stepId];

            switch (step.Type)
            {
                case StepType.Task:
                    return new Step
                    {
                        TaskId = step.TaskId.Value,
                        StepId = step.Id,
                        Name = step.Name
                    };

                case StepType.Condition:
                    return CheckCondition(step);

                case StepType.Switch:
                    return CheckSwitch(step);

                default:
                    throw new ArgumentException("step");
            }
        }
        
        private Step CheckCondition(_Step step)
        {
            var condition = (ConditionNext)step.Next;
            if (OnCondition(condition.Condition))
            {
                if (condition.TrueNext != null)
                {
                    return GetStep(condition.TrueNext.Value);
                }

                return null;
            }
            else
            {
                if (condition.FalseNext != null)
                {
                    return GetStep(condition.FalseNext.Value);
                }

                return null;
            }
        }

        private Step CheckSwitch(_Step step)
        {
            var _switch = step.Next as SwitchNext;
            var input = OnGetInput(_switch.InputExpression);

            if (!_switch.KeysNext.ContainsKey(input))
            {
                if (_switch.DefaultNext != null)
                    return GetStep(_switch.DefaultNext.Value);

                return null;
            }

            var next = _switch.KeysNext[input];
            if (next != null)
                return GetStep(next.Value);

            return null;
        }

        public static Flowchart Create(
            int flowId,
            CheckCallback onCondition,
            GetInput onGetInput)
        {
            string filename = HttpRuntime.AppDomainAppPath + ConfigurationManager.AppSettings["flow_" + flowId];
            return new Flowchart(filename, onCondition, onGetInput);
        }

        private static int? GetNodeValue(XElement node)
        {
            if (node != null && !string.IsNullOrWhiteSpace(node.Value))
                return node.Value.ToInt32();
            return null;
        }
    }
}
